﻿//
// Message.cs
//
// Author: Kees van Spelde <sicos2002@hotmail.com>
//
// Copyright (c) 2013-2018 Magic-Sessions. (www.magic-sessions.com)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Security.Cryptography.Pkcs;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using MsgReader.Exceptions;
using MsgReader.Helpers;
using MsgReader.Localization;
using MsgReader.Mime.Header;
using OpenMcdf;

namespace MsgReader.Outlook
{
    #region Enum MessageType
    /// <summary>
    /// The message types
    /// </summary>
    public enum MessageType
    {
        /// <summary>
        /// The message type is unknown
        /// </summary>
        Unknown,

        /// <summary>
        /// The message is a normal E-mail
        /// </summary>
        Email,

        /// <summary>
        /// Non-delivery report for a standard E-mail (REPORT.IPM.NOTE.NDR)
        /// </summary>
        EmailNonDeliveryReport,

        /// <summary>
        /// Delivery receipt for a standard E-mail (REPORT.IPM.NOTE.DR)
        /// </summary>
        EmailDeliveryReport,

        /// <summary>
        /// Delivery receipt for a delayed E-mail (REPORT.IPM.NOTE.DELAYED)
        /// </summary>
        EmailDelayedDeliveryReport,

        /// <summary>
        /// Read receipt for a standard E-mail (REPORT.IPM.NOTE.IPNRN)
        /// </summary>
        EmailReadReceipt,

        /// <summary>
        /// Non-read receipt for a standard E-mail (REPORT.IPM.NOTE.IPNNRN)
        /// </summary>
        EmailNonReadReceipt,

        /// <summary>
        /// The message in an E-mail that is encrypted and can also be signed (IPM.Note.SMIME)
        /// </summary>
        EmailEncryptedAndMaybeSigned,

        /// <summary>
        /// Non-delivery report for a Secure MIME (S/MIME) encrypted and opaque-signed E-mail (REPORT.IPM.NOTE.SMIME.NDR)
        /// </summary>
        EmailEncryptedAndMaybeSignedNonDelivery,

        /// <summary>
        /// Delivery report for a Secure MIME (S/MIME) encrypted and opaque-signed E-mail (REPORT.IPM.NOTE.SMIME.DR)
        /// </summary>
        EmailEncryptedAndMaybeSignedDelivery,
        
        /// <summary>
        /// The message is an E-mail that is clear signed (IPM.Note.SMIME.MultipartSigned)
        /// </summary>
        EmailClearSigned,

        /// <summary>
        /// The message is a secure read receipt for an E-mail (IPM.Note.Receipt.SMIME)
        /// </summary>
        EmailClearSignedReadReceipt,

        /// <summary>
        /// Non-delivery report for an S/MIME clear-signed E-mail (REPORT.IPM.NOTE.SMIME.MULTIPARTSIGNED.NDR)
        /// </summary>
        EmailClearSignedNonDelivery,

        /// <summary>
        /// Delivery receipt for an S/MIME clear-signed E-mail (REPORT.IPM.NOTE.SMIME.MULTIPARTSIGNED.DR)
        /// </summary>
        EmailClearSignedDelivery,

        /// <summary>
        /// The message is an E-mail that is generared signed (IPM.Note.BMA.Stub)
        /// </summary>
        EmailBmaStub,

        /// <summary>
        /// The message is a short message service (IPM.Note.Mobile.SMS)
        /// </summary>
        EmailSms,

        /// <summary>
        /// The message is an appointment (IPM.Appointment)
        /// </summary>
        Appointment,

        /// <summary>
        /// The message is a notification for an appointment (IPM.Notification.Meeting)
        /// </summary>
        AppointmentNotification,

        /// <summary>
        /// The message is a schedule for an appointment (IPM.Schedule.Meeting)
        /// </summary>
        AppointmentSchedule,

        /// <summary>
        /// The message is a request for an appointment (IPM.Schedule.Meeting.Request)
        /// </summary>
        AppointmentRequest,

        /// <summary>
        /// The message is a request for an appointment (REPORT.IPM.SCHEDULE.MEETING.REQUEST.NDR)
        /// </summary>
        AppointmentRequestNonDelivery,

        /// <summary>
        /// The message is a response to an appointment (IPM.Schedule.Response)
        /// </summary>
        AppointmentResponse,

        /// <summary>
        /// The message is a positive response to an appointment (IPM.Schedule.Resp.Pos)
        /// </summary>
        AppointmentResponsePositive,

        /// <summary>
        /// Non-delivery report for a positive meeting response (accept) (REPORT.IPM.SCHEDULE.MEETING.RESP.POS.NDR)
        /// </summary>
        AppointmentResponsePositiveNonDelivery,

        /// <summary>
        /// The message is a negative response to an appointment (IPM.Schedule.Resp.Neg)
        /// </summary>
        AppointmentResponseNegative,

        /// <summary>
        /// Non-delivery report for a negative meeting response (declinet) (REPORT.IPM.SCHEDULE.MEETING.RESP.NEG.NDR)
        /// </summary>
        AppointmentResponseNegativeNonDelivery,

        /// <summary>
        /// The message is a response to tentatively accept the meeting request (IPM.Schedule.Meeting.Resp.Tent)
        /// </summary>
        AppointmentResponseTentative,
        
        /// <summary>
        /// Non-delivery report for a Tentative meeting response (REPORT.IPM.SCHEDULE.MEETING.RESP.TENT.NDR)
        /// </summary>
        AppointmentResponseTentativeNonDelivery,

        /// <summary>
        /// The message is a cancelation an appointment (IPM.Schedule.Meeting.Canceled)
        /// </summary>
        AppointmentResponseCanceled,

        /// <summary>
        /// Non-delivery report for a cancelled meeting notification (REPORT.IPM.SCHEDULE.MEETING.CANCELED.NDR)
        /// </summary>
        AppointmentResponseCanceledNonDelivery,

        /// <summary>
        /// The message is a contact card (IPM.Contact)
        /// </summary>
        Contact,

        /// <summary>
        /// The message is a task (IPM.Task)
        /// </summary>
        Task,

        /// <summary>
        /// The message is a task request accept (IPM.TaskRequest.Accept)
        /// </summary>
        TaskRequestAccept,

        /// <summary>
        /// The message is a task request decline (IPM.TaskRequest.Decline)
        /// </summary>
        TaskRequestDecline,

        /// <summary>
        /// The message is a task request update (IPM.TaskRequest.Update)
        /// </summary>
        TaskRequestUpdate,

        /// <summary>
        /// The message is a sticky note (IPM.StickyNote)
        /// </summary>
        StickyNote,

        /// <summary>
        /// The message is Cisco Unity Voice message (IPM.Note.Custom.Cisco.Unity.Voice)
        /// </summary>
        CiscoUnityVoiceMessage,

        /// <summary>
        /// IPM.NOTE.RIGHTFAX.ADV
        /// </summary>
        RightFaxAdv,

        /// <summary>
        /// The message is Skype for Business missed message (IPM.Note.Microsoft.Missed)
        /// </summary>
        SkypeForBusinessMissedMessage,

        /// <summary>
        /// The message is a Skype for Business conversation (IPM.Note.Microsoft.Conversation)
        /// </summary>
        SkypeForBusinessConversation
    }
    #endregion

    #region Enum MessageImportance
    /// <summary>
    /// The importancy of the message
    /// </summary>
    public enum MessageImportance
    {
        /// <summary>
        /// Low
        /// </summary>
        Low = 0,

        /// <summary>
        /// Normal
        /// </summary>
        Normal = 1,

        /// <summary>
        /// High
        /// </summary>
        High = 2
    }
    #endregion

    public partial class Storage
    {
        /// <summary>
        /// Class represent a MSG object
        /// </summary>
        public class Message : Storage
        {
            #region Fields
            /// <summary>
            /// The name of the <see cref="CFStorage"/> stream that contains this message
            /// </summary>
            internal string StorageName { get; }

            /// <summary>
            /// Contains the <see cref="MessageType"/> of this Message
            /// </summary>
            private MessageType _type = MessageType.Unknown;

            /// <summary>
            /// contains the name of the <see cref="Storage.Message"/> file
            /// </summary>
            private string _fileName;

            /// <summary>
            /// Contains the date and time when the message was created or null
            /// when not available
            /// </summary>
            private DateTime? _creationTime;

            /// <summary>
            /// Contains the name of the last user (or creator) that has changed the Message object or
            /// null when not available
            /// </summary>
            private string _lastModifierName;

            /// <summary>
            /// Contains the date and time when the message was created or null
            /// when not available
            /// </summary>
            private DateTime? _lastModificationTime;

            /// <summary>
            /// contains all the <see cref="Storage.Recipient"/> objects
            /// </summary>
            private readonly List<Recipient> _recipients = new List<Recipient>();

            /// <summary>
            /// Contains an URL to the help page of a mailing list
            /// </summary>
            private string _mailingListHelp;

            /// <summary>
            /// Contains an URL to the subscribe page of a mailing list
            /// </summary>
            private string _mailingListSubscribe;

            /// <summary>
            /// Contains an URL to the unsubscribe page of a mailing list
            /// </summary>
            private string _mailingListUnsubscribe;

            /// <summary>
            /// Contains the date/time in UTC format when the <see cref="Storage.Message"/> object has been sent,
            /// null when not available
            /// </summary>
            private DateTime? _sentOn;

            /// <summary>
            /// Contains the date/time in UTC format when the <see cref="Storage.Message"/> object has been received,
            /// null when not available
            /// </summary>
            private DateTime? _receivedOn;

            /// <summary>
            /// Contains the <see cref="MessageImportance"/> of the <see cref="Storage.Message"/> object,
            /// null when not available
            /// </summary>
            private MessageImportance? _importance;

            /// <summary>
            /// Contains all the <see cref="Storage.Attachment"/> and <see cref="Storage.Message"/> objects.
            /// </summary>
            private readonly List<object> _attachments = new List<object>();

            /// <summary>
            /// Contains the subject prefix of the <see cref="Storage.Message"/> object
            /// </summary>
            private string _subjectPrefix;

            /// <summary>
            /// Contains the subject of the <see cref="Storage.Message"/> object
            /// </summary>
            private string _subject;

            /// <summary>
            /// Contains the normalized subject of the <see cref="Storage.Message"/> object
            /// </summary>
            private string _subjectNormalized;

            /// <summary>
            /// Contains the text body of the <see cref="Storage.Message"/> object
            /// </summary>
            private string _bodyText;

            /// <summary>
            /// Contains the html body of the <see cref="Storage.Message"/> object
            /// </summary>
            private string _bodyHtml;

            /// <summary>
            /// Contains the rtf body of the <see cref="Storage.Message"/> object
            /// </summary>
            private string _bodyRtf;

            /// <summary>
            /// Contains the <see cref="Encoding"/> that is used for the <see cref="BodyText"/> or <see cref="BodyHtml"/>. 
            /// It will contain null when the codepage could not be read from the <see cref="Storage.Message"/>
            /// </summary>
            private Encoding _internetCodepage;

            /// <summary>
            /// Contains the <see cref="Encoding"/> that is used for the <see cref="BodyRtf"/>.
            /// It will contain null when the codepage could not be read from the <see cref="Storage.Message"/>
            /// </summary>
            private Encoding _messageCodepage;

            /// <summary>
            /// Contains the the Windows LCID of the end user who created this <see cref = "Storage.Message" />
            /// </summary>
            private RegionInfo _messageLocalId;

            /// <summary>
            /// Contains the <see cref="Storage.Flag"/> object
            /// </summary>
            private Flag _flag;

            /// <summary>
            /// Contains the <see cref="Storage.Task"/> object
            /// </summary>
            private Task _task;

            /// <summary>
            /// Contains the <see cref="Storage.Appointment"/> object
            /// </summary>
            private Appointment _appointment;

            /// <summary>
            /// Contains the <see cref="Storage.Contact"/> object
            /// </summary>
            private Contact _contact;

            /// <summary>
            /// Contains the <see cref="Storage.ReceivedBy"/> object
            /// </summary>
            private ReceivedBy _receivedBy;

            /// <summary>
            /// The conversation index
            /// </summary>
            private string _conversationIndex;

            /// <summary>
            /// The conversation topic
            /// </summary>
            private string _conversationTopic;

            /// <summary>
            /// The message size
            /// </summary>
            private int? _messageSize;

            /// <summary>
            /// The transport message headers
            /// </summary>
            private string _TransportMessageHeaders;

            #endregion

            #region Properties
            /// <summary>
            /// Returns the ID of the message when the MSG file has been sent across the internet 
            /// (as specified in [RFC2822]). Null when not available
            /// </summary>
            public string Id => GetMapiPropertyString(MapiTags.PR_INTERNET_MESSAGE_ID);

            #region Type
            /// <summary>
            /// Gives the <see cref="MessageType">type</see> of this message object
            /// </summary>
            public MessageType Type
            {
                get
                {
                    if (_type != MessageType.Unknown)
                        return _type;

                    var type = GetMapiPropertyString(MapiTags.PR_MESSAGE_CLASS);

                    if (type == null)
                        return MessageType.Unknown;

                    switch (type.ToUpperInvariant())
                    {
                        case "IPM.NOTE":
                            _type = MessageType.Email;
                            break;

                        case "IPM.NOTE.MOBILE.SMS":
                            _type = MessageType.EmailSms;
                            break;

                        case "REPORT.IPM.NOTE.NDR":
                            _type = MessageType.EmailNonDeliveryReport;
                            break;

                        case "REPORT.IPM.NOTE.DR":
                            _type = MessageType.EmailDeliveryReport;
                            break;

                        case "REPORT.IPM.NOTE.DELAYED":
                            _type = MessageType.EmailDelayedDeliveryReport;
                            break;

                        case "REPORT.IPM.NOTE.IPNRN":
                            _type = MessageType.EmailReadReceipt;
                            break;

                        case "REPORT.IPM.NOTE.IPNNRN":
                            _type = MessageType.EmailNonReadReceipt;
                            break;

                        case "IPM.NOTE.SMIME":
                            _type = MessageType.EmailEncryptedAndMaybeSigned;
                            break;

                        case "REPORT.IPM.NOTE.SMIME.NDR":
                            _type = MessageType.EmailEncryptedAndMaybeSignedNonDelivery;
                            break;

                        case "REPORT.IPM.NOTE.SMIME.DR":
                            _type = MessageType.EmailEncryptedAndMaybeSignedDelivery;
                            break;

                        case "IPM.NOTE.SMIME.MULTIPARTSIGNED":
                            _type = MessageType.EmailClearSigned;
                            break;

                        case "IPM.NOTE.RECEIPT.SMIME.MULTIPARTSIGNED":
                            _type = MessageType.EmailClearSigned;
                            break;

                        case "REPORT.IPM.NOTE.SMIME.MULTIPARTSIGNED.NDR":
                            _type = MessageType.EmailClearSignedNonDelivery;
                            break;

                        case "REPORT.IPM.NOTE.SMIME.MULTIPARTSIGNED.DR":
                            _type = MessageType.EmailClearSignedDelivery;
                            break;

                        case "IPM.NOTE.BMA.STUB":
                            _type = MessageType.EmailBmaStub;
                            break;

                        case "IPM.APPOINTMENT":
                            _type = MessageType.Appointment;
                            break;

                        case "IPM.SCHEDULE.MEETING":
                            _type = MessageType.AppointmentSchedule;
                            break;

                        case "IPM.NOTIFICATION.MEETING":
                            _type = MessageType.AppointmentNotification;
                            break;

                        case "IPM.SCHEDULE.MEETING.REQUEST":
                            _type = MessageType.AppointmentRequest;
                            break;

                        case "IPM.SCHEDULE.MEETING.REQUEST.NDR":
                            _type = MessageType.AppointmentRequestNonDelivery;
                            break;

                        case "IPM.SCHEDULE.MEETING.CANCELED":
                            _type = MessageType.AppointmentResponseCanceled;
                            break;

                        case "IPM.SCHEDULE.MEETING.CANCELED.NDR":
                            _type = MessageType.AppointmentResponseCanceledNonDelivery;
                            break;
                        
                        case "IPM.SCHEDULE.MEETING.RESPONSE":
                            _type = MessageType.AppointmentResponse;
                            break;

                        case "IPM.SCHEDULE.MEETING.RESP.POS":
                            _type = MessageType.AppointmentResponsePositive;
                            break;

                        case "IPM.SCHEDULE.MEETING.RESP.POS.NDR":
                            _type = MessageType.AppointmentResponsePositiveNonDelivery;
                            break;

                        case "IPM.SCHEDULE.MEETING.RESP.NEG":
                            _type = MessageType.AppointmentResponseNegative;
                            break;

                        case "IPM.SCHEDULE.MEETING.RESP.NEG.NDR":
                            _type = MessageType.AppointmentResponseNegativeNonDelivery;
                            break;

                        case "IPM.SCHEDULE.MEETING.RESP.TENT":
                            _type = MessageType.AppointmentResponseTentative;
                            break;

                        case "IPM.SCHEDULE.MEETING.RESP.TENT.NDR":
                            _type = MessageType.AppointmentResponseTentativeNonDelivery;
                            break;

                        case "IPM.CONTACT":
                            _type = MessageType.Contact;
                            break;

                        case "IPM.TASK":
                            _type = MessageType.Task;
                            break;

                        case "IPM.TASKREQUEST.ACCEPT":
                            _type = MessageType.TaskRequestAccept;
                            break;

                        case "IPM.TASKREQUEST.DECLINE":
                            _type = MessageType.TaskRequestDecline;
                            break;

                        case "IPM.TASKREQUEST.UPDATE":
                            _type = MessageType.TaskRequestUpdate;
                            break;

                        case "IPM.STICKYNOTE":
                            _type = MessageType.StickyNote;
                            break;

                        case "IPM.NOTE.CUSTOM.CISCO.UNITY.VOICE":
                            _type = MessageType.CiscoUnityVoiceMessage;
                            break;

                        case "IPM.NOTE.RIGHTFAX.ADV":
                            _type = MessageType.RightFaxAdv;
                            break;
                        
                        case "IPM.NOTE.MICROSOFT.MISSED":
                            _type = MessageType.SkypeForBusinessMissedMessage;
                            break;

                        case "IPM.NOTE.MICROSOFT.CONVERSATION":
                            _type = MessageType.SkypeForBusinessConversation;
                            break;
                    }

                    return _type;
                }
            }
            #endregion

            /// <summary>
            /// Returns the filename of the message object. For message objects Outlook uses the subject. It strips
            /// invalid filename characters. When there is no filename the name from <see cref="LanguageConsts.NameLessFileName"/>
            /// will be used
            /// </summary>
            public string FileName
            {
                get
                {
                    if (_fileName != null)
                        return _fileName;

                    _fileName = GetMapiPropertyString(MapiTags.PR_SUBJECT);

                    if (string.IsNullOrEmpty(_fileName))
                        _fileName = LanguageConsts.NameLessFileName;

                    _fileName = FileManager.RemoveInvalidFileNameChars(_fileName) + ".msg";
                    return _fileName;
                }
            }

            /// <summary>
            /// Returns the date and time when the message was created or null
            /// when not available
            /// </summary>
            public DateTime? CreationTime => _creationTime ?? (_creationTime = GetMapiPropertyDateTime(MapiTags.PR_CREATION_TIME));

            /// <summary>
            /// Returns the name of the last user (or creator) that has changed the Message object or
            /// null when not available
            /// </summary>
            public string LastModifierName => _lastModifierName ??
                                                (_lastModifierName = GetMapiPropertyString(MapiTags.PR_LAST_MODIFIER_NAME_W));

            /// <summary>
            /// Returns the date and time when the message was last modified or null
            /// when not available
            /// </summary>
            public DateTime? LastModificationTime => _lastModificationTime ??
                                                        (_lastModificationTime = GetMapiPropertyDateTime(MapiTags.PR_LAST_MODIFICATION_TIME));

            /// <summary>
            /// Returns the raw Transport Message Headers
            /// </summary>
            public string TransportMessageHeaders => _TransportMessageHeaders;
            /// <summary>
            /// Returns the sender of the Message
            /// </summary>
            // ReSharper disable once CSharpWarnings::CS0109
            public new Sender Sender { get; private set; }

            /// <summary>
            /// Returns the representing sender of the Message, null when not available
            /// </summary>
            // ReSharper disable once CSharpWarnings::CS0109
            public new SenderRepresenting SenderRepresenting { get; private set; }
            
            /// <summary>
            /// Returns the list of recipients in the message object
            /// </summary>
            public List<Recipient> Recipients => _recipients;

            /// <summary>
            /// Returns an URL to the help page of an mailing list when this message is part of a mailing
            /// or null when not available
            /// </summary>
            public string MailingListHelp
            {
                get
                {
                    if (!string.IsNullOrEmpty(_mailingListHelp))
                        return _mailingListHelp;

                    _mailingListHelp = GetMapiPropertyString(MapiTags.PR_LIST_HELP);

                    if (_mailingListHelp == null) return null;

                    if (_mailingListHelp.StartsWith("<"))
                        _mailingListHelp = _mailingListHelp.Substring(1);

                    if (_mailingListHelp.EndsWith(">"))
                        _mailingListHelp = _mailingListHelp.Substring(0, _mailingListHelp.Length - 1);

                    return _mailingListHelp;
                }
            }

            /// <summary>
            /// Returns an URL to the subscribe page of an mailing list when this message is part of a mailing
            /// or null when not available
            /// </summary>
            public string MailingListSubscribe
            {
                get
                {
                    if (!string.IsNullOrEmpty(_mailingListSubscribe))
                        return _mailingListSubscribe;

                    _mailingListSubscribe = GetMapiPropertyString(MapiTags.PR_LIST_SUBSCRIBE);

                    if (_mailingListSubscribe == null) return null;

                    if (_mailingListSubscribe.StartsWith("<"))
                        _mailingListSubscribe = _mailingListSubscribe.Substring(1);

                    if (_mailingListSubscribe.EndsWith(">"))
                        _mailingListSubscribe = _mailingListSubscribe.Substring(0, _mailingListSubscribe.Length - 1);

                    return _mailingListSubscribe;
                }
            }

            /// <summary>
            /// Returns an URL to the unsubscribe page of an mailing list when this message is part of a mailing
            /// </summary>
            public string MailingListUnsubscribe
            {
                get
                {
                    if (!string.IsNullOrEmpty(_mailingListUnsubscribe))
                        return _mailingListUnsubscribe;

                    _mailingListUnsubscribe = GetMapiPropertyString(MapiTags.PR_LIST_UNSUBSCRIBE);

                    if (_mailingListUnsubscribe == null) return null;

                    if (_mailingListUnsubscribe.StartsWith("<"))
                        _mailingListUnsubscribe = _mailingListUnsubscribe.Substring(1);

                    if (_mailingListUnsubscribe.EndsWith(">"))
                        _mailingListUnsubscribe = _mailingListUnsubscribe.Substring(0, _mailingListUnsubscribe.Length - 1);

                    return _mailingListUnsubscribe;
                }
            }

            /// <summary>
            /// Returns the date/time in UTC format when the message object has been sent, null when not available
            /// </summary>
            public DateTime? SentOn
            {
                get
                {
                    if (_sentOn != null)
                        return _sentOn;

                    _sentOn = GetMapiPropertyDateTime(MapiTags.PR_CLIENT_SUBMIT_TIME) ??
                                    GetMapiPropertyDateTime(MapiTags.PR_PROVIDER_SUBMIT_TIME);

                    if (_sentOn == null && Headers != null)
                        _sentOn = Headers.DateSent.ToLocalTime();

                    return _sentOn;
                }
            }

            /// <summary>
            /// PR_MESSAGE_DELIVERY_TIME  is the time that the message was delivered to the store and 
            /// PR_CLIENT_SUBMIT_TIME  is the time when the message was sent by the client (Outlook) to the server.
            /// Now in this case when the Outlook is offline, it refers to the local store. Therefore when an email is sent, 
            /// it gets submitted to the local store and PR_MESSAGE_DELIVERY_TIME  gets set the that time. Once the Outlook is 
            /// online at that point the message gets submitted by the client to the server and the PR_CLIENT_SUBMIT_TIME  gets stamped. 
            /// Null when not available
            /// </summary>
            public DateTime? ReceivedOn
            {
                get
                {
                    if (_receivedOn != null)
                        return _receivedOn;

                    _receivedOn = GetMapiPropertyDateTime(MapiTags.PR_MESSAGE_DELIVERY_TIME);

                    if (_receivedOn == null && Headers?.Received != null && Headers.Received.Count > 0)
                        _receivedOn = Headers.Received[0].Date.ToLocalTime();

                    return _receivedOn;
                }
            }

            /// <summary>
            /// Returns the <see cref="MessageImportance"/> of the <see cref="Storage.Message"/> object, null when not available
            /// </summary>
            public MessageImportance? Importance
            {
                get
                {
                    if (_importance != null)
                        return _importance;

                    var importance = GetMapiPropertyInt32(MapiTags.PR_IMPORTANCE);
                    if (importance == null)
                    {
                        _importance = MessageImportance.Normal;
                        return _importance;
                    }

                    switch (importance)
                    {
                        case 0:
                            _importance = MessageImportance.Low;
                            break;

                        case 1:
                            _importance = MessageImportance.Normal;
                            break;

                        case 2:
                            _importance = MessageImportance.High;
                            break;
                    }

                    return _importance;
                }
            }

            /// <summary>
            /// Returns the <see cref="MessageImportance"/> of the <see cref="Storage.Message"/> object object as text
            /// </summary>
            public string ImportanceText
            {
                get
                {
                    if (Importance == null)
                        return LanguageConsts.ImportanceNormalText;

                    switch (Importance)
                    {
                        case MessageImportance.Low:
                            return LanguageConsts.ImportanceLowText;

                        case MessageImportance.Normal:
                            return LanguageConsts.ImportanceNormalText;

                        case MessageImportance.High:
                            return LanguageConsts.ImportanceHighText;

                    }

                    return LanguageConsts.ImportanceNormalText;
                }
            }

            /// <summary>
            /// Returns a list with <see cref="Storage.Attachment"/> and/or <see cref="Storage.Message"/> 
            /// objects that are attachted to the <see cref="Storage.Message"/> object
            /// </summary>
            public List<Object> Attachments => _attachments;

            /// <summary>
            /// Returns the rendering position of this <see cref="Storage.Message"/> object when it was added to another
            /// <see cref="Storage.Message"/> object and the body type was set to RTF
            /// </summary>
            public int RenderingPosition { get; }

            /// <summary>
            ///     Returns or sets the subject prefix of the E-mail
            /// </summary>
            public string SubjectPrefix
            {
                get
                {
                    if (_subjectPrefix != null)
                        return _subjectPrefix;

                    _subjectPrefix = GetMapiPropertyString(MapiTags.PR_SUBJECT_PREFIX);
                    if (string.IsNullOrEmpty(_subjectPrefix))
                        _subjectPrefix = string.Empty;

                    return _subjectPrefix;
                }
            }

            /// <summary>
            /// Returns the subject of the <see cref="Storage.Message"/> object
            /// </summary>
            public string Subject
            {
                get
                {
                    if (_subject != null)
                        return _subject;

                    _subject = GetMapiPropertyString(MapiTags.PR_SUBJECT);
                    if (string.IsNullOrEmpty(_subject))
                        _subject = string.Empty;

                    return _subject;
                }
            }

            /// <summary>
            ///     Returns the normalized subject of the E-mail
            /// </summary>
            public string SubjectNormalized
            {
                get
                {
                    if (_subjectNormalized != null)
                        return _subjectNormalized;

                    _subjectNormalized = GetMapiPropertyString(MapiTags.PR_NORMALIZED_SUBJECT);
                    if (string.IsNullOrEmpty(_subjectNormalized))
                        _subjectNormalized = string.Empty;

                    return _subjectNormalized;
                }
            }

            /// <summary>
            /// Returns the available E-mail headers. These are only filled when the message
            /// has been sent accross the internet. Returns null when there aren't any message headers
            /// </summary>
            public MessageHeader Headers { get; private set; }

            // ReSharper disable once CSharpWarnings::CS0109
            /// <summary>
            /// Returns a <see cref="Flag"/> object when a flag has been set on the <see cref="Storage.Message"/>.
            /// Returns null when not available.
            /// </summary>
            public new Flag Flag
            {
                get
                {
                    if (_flag != null)
                        return _flag;

                    var flag = new Flag(this);

                    if (flag.Request != null)
                        _flag = flag;

                    return _flag;
                }
            }

            // ReSharper disable once CSharpWarnings::CS0109
            /// <summary>
            /// Returns an <see cref="Appointment"/> object when the <see cref="MessageType"/> is a <see cref="MessageType.Appointment"/>.
            /// Returns null when not available.
            /// </summary>
            public new Appointment Appointment
            {
                get
                {
                    if (_appointment != null)
                        return _appointment;

                    switch (Type)
                    {
                        case MessageType.AppointmentRequest:
                        case MessageType.Appointment:
                        case MessageType.AppointmentResponse:
                        case MessageType.AppointmentResponsePositive:
                        case MessageType.AppointmentResponseNegative:
                            break;

                        default:
                            return null;
                    }

                    _appointment = new Appointment(this);
                    return _appointment;
                }
            }

            // ReSharper disable once CSharpWarnings::CS0109
            /// <summary>
            /// Returns a <see cref="Task"/> object. This property is only available when: <br/>
            /// - The <see cref="Storage.Message.Type"/> is an <see cref="Storage.Message.MessageType.Email"/> and the <see cref="Flag"/> object is not null<br/>
            /// - The <see cref="Storage.Message.Type"/> is an <see cref="Storage.Message.MessageType.Task"/> or <see cref="Storage.Message.MessageType.TaskRequestAccept"/> <br/>
            /// </summary>
            public new Task Task
            {
                get
                {
                    if (_task != null)
                        return _task;

                    switch (_type)
                    {
                        case MessageType.Email:
                            if (Flag == null)
                                return null;
                            break;

                        case MessageType.Task:
                        case MessageType.TaskRequestAccept:
                            break;

                        default:
                            return null;
                    }

                    _task = new Task(this);
                    return _task;
                }
            }

            // ReSharper disable once CSharpWarnings::CS0109
            /// <summary>
            /// Returns a <see cref="Storage.Contact"/> object when the <see cref="MessageType"/> is a <see cref="MessageType.Contact"/>.
            /// Returns null when not available.
            /// </summary>
            public new Contact Contact
            {
                get
                {
                    if (_contact != null)
                        return _contact;

                    switch (Type)
                    {
                        case MessageType.Contact:
                            break;

                        default:
                            return null;
                    }

                    _contact = new Contact(this);
                    return _contact;
                }
            }

            /// <summary>
            /// Returns the categories that are placed in the Outlook message.
            /// Only supported for outlook messages from Outlook 2007 or higher
            /// </summary>
            public ReadOnlyCollection<string> Categories => GetMapiPropertyStringList(MapiTags.Keywords);

            /// <summary>
            /// Returns the body of the Outlook message in plain text format.
            /// </summary>
            /// <value> The body of the Outlook message in plain text format. </value>
            public string BodyText
            {
                get
                {
                    if (_bodyText != null)
                        return _bodyText;

                    _bodyText = GetMapiPropertyString(MapiTags.PR_BODY);
                    return _bodyText;
                }
            }

            /// <summary>
            /// Returns the body of the Outlook message in RTF format.
            /// </summary>
            /// <value> The body of the Outlook message in RTF format. </value>
            public string BodyRtf
            {
                get
                {
                    if (_bodyRtf != null)
                        return _bodyRtf;

                    // Get value for the RTF compressed MAPI property
                    var rtfBytes = GetMapiPropertyBytes(MapiTags.PR_RTF_COMPRESSED);

                    // Return null if no property value exists
                    if (rtfBytes == null || rtfBytes.Length == 0)
                        return null;

                    rtfBytes = RtfDecompressor.DecompressRtf(rtfBytes);
                    _bodyRtf = MessageCodePage.GetString(rtfBytes);
                    return _bodyRtf;
                }
            }

            /// <summary>
            /// Returns the body of the Outlook message in HTML format.
            /// </summary>
            /// <value> The body of the Outlook message in HTML format. </value>
            public string BodyHtml
            {
                get
                {
                    if (_bodyHtml != null)
                        return _bodyHtml;

                    // Get value for the HTML MAPI property
                    var htmlObject = GetMapiProperty(MapiTags.PR_BODY_HTML);
                    string html = null;
                    
                    if (htmlObject is string)
                        html = htmlObject as string;
                    else if (htmlObject is byte[])
                    {
                        var htmlByteArray = htmlObject as byte[];
                        html = InternetCodePage.GetString(htmlByteArray);
                    }

                    // When there is no HTML found
                    if (html == null)
                    {
                        // Check if we have HTML embedded into rtf
                        var bodyRtf = BodyRtf;
                        if (bodyRtf != null)
                        {
                            var rtfDomDocument = new Rtf.DomDocument();
                            rtfDomDocument.LoadRtfText(bodyRtf);
                            if (!string.IsNullOrEmpty(rtfDomDocument.HtmlContent))
                                html = rtfDomDocument.HtmlContent.Trim('\r', '\n');
                        }
                    }

                    _bodyHtml = html;
                    return _bodyHtml;
                }
            }

            /// <summary>
            /// Returns the <see cref="Encoding"/> that is used for the <see cref="BodyText"/>
            /// or <see cref="BodyHtml"/>. It will return <see cref="MessageLocalId"/> when the 
            /// codepage could not be read from the <see cref="Storage.Message"/>
            /// <remarks>
            /// See the <see cref="MessageCodePage"/> property when dealing with the <see cref="BodyRtf"/>
            /// </remarks>
            /// </summary>
            public Encoding InternetCodePage
            {
                get
                {
                    if (_internetCodepage != null)
                        return _internetCodepage;

                    var codePage = GetMapiPropertyInt32(MapiTags.PR_INTERNET_CPID);
                    _internetCodepage = codePage == null ? Encoding.Default : Encoding.GetEncoding((int)codePage);
                    return _internetCodepage;
                }
            }

            /// <summary>
            /// Returns the <see cref="Encoding"/> that is used for the <see cref="BodyRtf"/>.
            /// It will return the systems default encoding when the codepage could not be read from 
            /// the <see cref="Storage.Message"/>
            /// <remarks>
            /// See the <see cref="InternetCodePage"/> property when dealing with the <see cref="BodyRtf"/>
            /// </remarks>
            /// </summary>
            public Encoding MessageCodePage
            {
                get
                {
                    if (_messageCodepage != null)
                        return _messageCodepage;

                    var codePage = GetMapiPropertyInt32(MapiTags.PR_MESSAGE_CODEPAGE);

                    try
                    {
                        _messageCodepage = codePage != null ? Encoding.GetEncoding((int)codePage) : InternetCodePage;
                    }
                    catch (NotSupportedException)
                    {
                        _messageCodepage = InternetCodePage;
                    }

                    return _messageCodepage;
                }
            }

            /// <summary>
            /// Returns the the <see cref="RegionInfo"/> for the Windows LCID of the end user who created this
            /// <see cref="Storage.Message"/> It will return <c>null</c> when the the Windows LCID could not be 
            /// read from the <see cref="Storage.Message"/>
            /// </summary>
            public RegionInfo MessageLocalId
            {
                get
                {
                    if (_messageLocalId != null)
                        return _messageLocalId;

                    var lcid = GetMapiPropertyInt32(MapiTags.PR_MESSAGE_LOCALE_ID);

                    if (!lcid.HasValue) return null;
                    _messageLocalId = new RegionInfo(lcid.Value);

                    return null;
                }
            }

            /// <summary>
            /// Returns true when the signature is valid when the <see cref="MessageType"/> is a <see cref="MessageType.EmailEncryptedAndMaybeSigned"/>.
            /// It will return null when the signature is invalid or the <see cref="Storage.Message"/> has another <see cref="MessageType"/>
            /// </summary>
            public bool? SignatureIsValid { get; private set; }

            /// <summary>
            /// Returns the name of the person who signed the <see cref="Storage.Message"/> when the <see cref="MessageType"/> is a 
            /// <see cref="MessageType.EmailEncryptedAndMaybeSigned"/>. It will return null when the signature is invalid or the <see cref="Storage.Message"/> 
            /// has another <see cref="MessageType"/>
            /// </summary>
            public string SignedBy { get; private set; }

            /// <summary>
            /// Returns the date and time when the <see cref="Storage.Message"/> has been signed when the <see cref="MessageType"/> is a 
            /// <see cref="MessageType.EmailEncryptedAndMaybeSigned"/>. It will return null when the signature is invalid or the <see cref="Storage.Message"/> 
            /// has another <see cref="MessageType"/>
            /// </summary>
            public DateTime? SignedOn { get; private set; }

            /// <summary>
            /// Returns the certificate that has been used to sign the <see cref="Storage.Message"/> when the <see cref="MessageType"/> is a 
            /// <see cref="MessageType.EmailEncryptedAndMaybeSigned"/>. It will return null when the signature is invalid or the <see cref="Storage.Message"/> 
            /// has another <see cref="MessageType"/>
            /// </summary>
            public X509Certificate2 SignedCertificate { get; private set; }
            
            /// <summary>
            /// Returns information about who has received this message. This information is only
            /// set when a message has been received and when the message provider stamped this 
            /// information into this message. Null when not available.
            /// </summary>
#pragma warning disable 109
            public new ReceivedBy ReceivedBy
#pragma warning restore 109
            {
                get
                {
                    if (_receivedBy != null)
                        return _receivedBy;

                    _receivedBy = new ReceivedBy(
                        GetMapiPropertyString(MapiTags.PR_RECEIVED_BY_ADDRTYPE), 
                        GetMapiPropertyString(MapiTags.PR_RECEIVED_BY_EMAIL_ADDRESS),
                        GetMapiPropertyString(MapiTags.PR_RECEIVED_BY_NAME));
                    return _receivedBy;
                }
            }

            /// <summary>
            /// Returns the index of the conversation. When not available <c>null</c> is returned
            /// </summary>
            public string ConversationIndex
            {
                get
                {
                    if (_conversationIndex != null)
                        return _conversationIndex;
                    var conversationIndexBytes= GetMapiProperty(MapiTags.PR_CONVERSATION_INDEX);
                    if(conversationIndexBytes != null && conversationIndexBytes is byte[])
                    {
                        _conversationIndex = BitConverter.ToString((byte[])conversationIndexBytes, 0);
                        if (!string.IsNullOrWhiteSpace(_conversationIndex) && _conversationIndex.Contains("-"))
                            _conversationIndex = _conversationIndex.Replace("-", "");

                    }
                    if (_conversationIndex == null)
                        _conversationIndex = string.Empty;

                    return _conversationIndex;
                }
            }

            /// <summary>
            /// Returns the topic of the conversation. When not available <c>null</c> is returned
            /// </summary>
            public string ConversationTopic
            {
                get
                {
                    if (_conversationTopic != null)
                        return _conversationTopic;

                    _conversationTopic = GetMapiPropertyString(MapiTags.PR_CONVERSATION_TOPIC);
                    return _conversationTopic;
                }
            }


            /// <summary>
            /// Returns the size of the message. When not available <c>null</c> is returned
            /// </summary>
            public int? Size
            {
                get
                {
                    if (_messageSize != null)
                        return _messageSize;

                    _messageSize = GetMapiPropertyInt32(MapiTags.PR_MESSAGE_SIZE);
                    return _messageSize;
                }
            }
            #endregion

            #region Constructors
            /// <summary>
            ///   Initializes a new instance of the <see cref="Storage.Message" /> class from a msg file.
            /// </summary>
            /// <param name="msgfile">The msg file to load</param>
            /// <param name="fileAccess">FileAcces mode, default is Read</param>
            public Message(string msgfile, FileAccess fileAccess = FileAccess.Read) : base(msgfile, fileAccess) { }

            /// <summary>
            /// Initializes a new instance of the <see cref="Storage.Message" /> class from a <see cref="Stream" /> containing an IStorage.
            /// </summary>
            /// <param name="storageStream"> The <see cref="Stream" /> containing an IStorage. </param>
            /// <param name="fileAccess">FileAcces mode, default is Read</param>
            public Message(Stream storageStream, FileAccess fileAccess = FileAccess.Read) : base(storageStream, fileAccess) { }

            /// <summary>
            /// Initializes a new instance of the <see cref="Storage.Message" /> class on the specified <see cref="CFStorage"/>.
            /// </summary>
            /// <param name="storage"> The storage to create the <see cref="Storage.Message" /> on. </param>
            /// <param name="renderingPosition"></param>
            /// <param name="storageName">The name of the <see cref="CFStorage"/> that contains this message</param>
            internal Message(CFStorage storage, int renderingPosition, string storageName) : base(storage)
            {
                StorageName = storageName;
                _propHeaderSize = MapiTags.PropertiesStreamHeaderTop;
                RenderingPosition = renderingPosition;
            }
            #endregion

            #region GetHeaders
            /// <summary>
            /// Try's to read the E-mail transport headers. They are only there when a msg file has been
            /// sent over the internet. When a message stays inside an Exchange server there are not any headers
            /// </summary>
            private void GetHeaders()
            {
                _TransportMessageHeaders = GetMapiPropertyString(MapiTags.PR_TRANSPORT_MESSAGE_HEADERS);
                if (!string.IsNullOrEmpty(_TransportMessageHeaders))
                    Headers = HeaderExtractor.GetHeaders(_TransportMessageHeaders);
            }
            #endregion

            #region LoadStorage
            /// <summary>
            /// Processes sub storages on the specified storage to capture attachment and recipient data.
            /// </summary>
            /// <param name="storage"> The storage to check for attachment and recipient data. </param>
            protected override void LoadStorage(CFStorage storage)
            {
                base.LoadStorage(storage);

                foreach (var storageStatistic in _subStorageStatistics)
                {
                    // Run specific load method depending on sub storage name prefix
                    if (storageStatistic.Key.StartsWith(MapiTags.RecipStoragePrefix))
                    {
                        var recipient = new Recipient(new Storage(storageStatistic.Value)); 
                        _recipients.Add(recipient);
                    }
                    else if (storageStatistic.Key.StartsWith(MapiTags.AttachStoragePrefix))
                    {
                        switch (Type)
                        {
                            case MessageType.EmailClearSigned:
                                LoadClearSignedMessage(storageStatistic.Value);
                                break;

                            case MessageType.EmailEncryptedAndMaybeSigned:
                                LoadEncryptedAndMeabySignedMessage(storageStatistic.Value);
                                break;

                            default:
                                LoadAttachmentStorage(storageStatistic.Value, storageStatistic.Key);
                                break;
                        }
                    }
                }

                GetHeaders();
                SetEmailSenderAndRepresentingSender();

                // Check if there is a named substorage and if so open it and map all the named MAPI properties
                if (_subStorageStatistics.ContainsKey(MapiTags.NameIdStorage))
                {
                    var mappingValues = new List<string>();

                    // Get all the named properties from the _streamStatistics
                    foreach (var streamStatistic in _streamStatistics)
                    {
                        var name = streamStatistic.Key;

                        if (name.StartsWith(MapiTags.SubStgVersion1))
                        {
                            // Get the property value
                            var propIdentString = name.Substring(12, 4);

                            // Convert it to a short
                            var value = ushort.Parse(propIdentString, NumberStyles.HexNumber);

                            // Check if the value is in the named property range (8000 to FFFE (Hex))
                            if (value >= 32768 && value <= 65534)
                            {
                                // If so then add it to perform mapping later on
                                if (!mappingValues.Contains(propIdentString))
                                    mappingValues.Add(propIdentString);
                            }
                        }
                    }

                    // Check if there is also a properties stream and if so get all the named MAPI properties from it
                    if (_streamStatistics.ContainsKey(MapiTags.PropertiesStream))
                    {
                        // Get the raw bytes for the property stream
                        var propBytes = GetStreamBytes(MapiTags.PropertiesStream);

                        for (var i = _propHeaderSize; i < propBytes.Length; i = i + 16)
                        {
                            // Get property identifer located in 3rd and 4th bytes as a hexdecimal string
                            var propIdent = new[] { propBytes[i + 3], propBytes[i + 2] };
                            var propIdentString = BitConverter.ToString(propIdent).Replace("-", string.Empty);

                            // Convert it to a short
                            var value = ushort.Parse(propIdentString, NumberStyles.HexNumber);

                            // Check if the value is in the named property range (8000 to FFFE (Hex))
                            if (value >= 32768 && value <= 65534)
                            {
                                // If so then add it to perform mapping later on
                                if (!mappingValues.Contains(propIdentString))
                                    mappingValues.Add(propIdentString);
                            }
                        }
                    }

                    // Check if there is something to map
                    if (mappingValues.Count <= 0) return;
                    // Get the Named Id Storage, we need this one to perform the mapping
                    var subStorage = _subStorageStatistics[MapiTags.NameIdStorage];

                    // Load the subStorage into our mapping class that does all the mapping magic
                    var mapiToOom = new MapiTagMapper(new Storage(subStorage));

                    // Get the mapped properties
                    _namedProperties = mapiToOom.GetMapping(mappingValues);
                }
            }
            #endregion

            #region ProcessSignedContent
            /// <summary>
            /// Processes the signed content
            /// </summary>
            /// <param name="data"></param>
            /// <returns></returns>
            private void ProcessSignedContent(byte[] data)
            {
                var signedCms = new SignedCms();
                signedCms.Decode(data);

                try
                {
                    //signedCms.CheckSignature(signedCms.Certificates, false);
                    foreach (var cert in signedCms.Certificates)
                        SignatureIsValid = cert.Verify();

                    SignatureIsValid = true;
                    foreach (var cryptographicAttributeObject in signedCms.SignerInfos[0].SignedAttributes)
                    {
                        if (cryptographicAttributeObject.Values[0] is Pkcs9SigningTime)
                        {
                            var pkcs9SigningTime = (Pkcs9SigningTime)cryptographicAttributeObject.Values[0];
                            SignedOn = pkcs9SigningTime.SigningTime.ToLocalTime();
                        }
                    }

                    var certificate = signedCms.SignerInfos[0].Certificate;
                    if (certificate != null)
                    {
                        SignedCertificate = certificate;
                        SignedBy = certificate.GetNameInfo(X509NameType.SimpleName, false);
                    }
                }
                catch (CryptographicException)
                {
                    SignatureIsValid = false;
                }

                // Get the decoded attachment
                using (var memoryStream = new MemoryStream(signedCms.ContentInfo.Content))
                {
                    var eml = Mime.Message.Load(memoryStream);
                    if (eml.TextBody != null)
                        _bodyText = eml.TextBody.GetBodyAsText();

                    if (eml.HtmlBody != null)
                        _bodyHtml = eml.HtmlBody.GetBodyAsText();

                    foreach (var emlAttachment in eml.Attachments)
                        _attachments.Add(new Attachment(emlAttachment));
                }
            }
#endregion

            #region LoadEncryptedSignedMessage
            /// <summary>
            /// Load's and parses a signed message. The signed message should be in an attachment called smime.p7m
            /// </summary>
            /// <param name="storage"></param>
            private void LoadEncryptedAndMeabySignedMessage(CFStorage storage)
            {
                // Create attachment from attachment storage
                var attachment = new Attachment(new Storage(storage), null);

                if (attachment.FileName.ToUpperInvariant() != "SMIME.P7M")
                    throw new MRInvalidSignedFile(
                        "The signed file is not valid, it should contain an attachment called smime.p7m but it didn't");

                ProcessSignedContent(attachment.Data);
            }
            #endregion

            #region LoadEncryptedSignedMessage
            /// <summary>
            /// Load's and parses a signed message
            /// </summary>
            /// <param name="storage"></param>
            private void LoadClearSignedMessage(CFStorage storage)
            {
                // Create attachment from attachment storage
                var attachment = new Attachment(new Storage(storage), null);

                // Get the decoded attachment
                using (var memoryStream = new MemoryStream(attachment.Data))
                {
                    var eml = Mime.Message.Load(memoryStream);
                    if (eml.TextBody != null)
                        _bodyText = eml.TextBody.GetBodyAsText();

                    if (eml.HtmlBody != null)
                    _bodyHtml = eml.HtmlBody.GetBodyAsText();

                    foreach (var emlAttachment in eml.Attachments)
                    {
                        if (emlAttachment.FileName.ToUpperInvariant() == "SMIME.P7S")
                            ProcessSignedContent(emlAttachment.Body);
                        else
                            _attachments.Add(new Attachment(emlAttachment));
                    }

                }
            }
            #endregion

            #region LoadAttachmentStorage
            /// <summary>
            /// Loads the attachment data out of the specified storage.
            /// </summary>
            /// <param name="storage"> The attachment storage. </param>
            /// <param name="storageName">The name of the <see cref="CFStorage"/></param>
            private void LoadAttachmentStorage(CFStorage storage, string storageName)
            {
                // Create attachment from attachment storage
                var attachment = new Attachment(new Storage(storage), storageName);
                
                var attachMethod = attachment.GetMapiPropertyInt32(MapiTags.PR_ATTACH_METHOD);
                switch (attachMethod)
                {
                    case MapiTags.ATTACH_EMBEDDED_MSG:
                        // Create new Message and set parent and header size
                        var subStorage = attachment.GetMapiProperty(MapiTags.PR_ATTACH_DATA_BIN) as CFStorage;
                        var subMsg = new Message(subStorage, attachment.RenderingPosition, storageName)
                        {
                            _parentMessage = this,
                            _propHeaderSize = MapiTags.PropertiesStreamHeaderEmbedded
                        };
                        _attachments.Add(subMsg);
                        break;

                    default:
                        // Add attachment to attachment list
                        _attachments.Add(attachment);
                        break;
                }
            }
            #endregion

            #region DeleteAttachment
            /// <summary>
            /// Removes the given <paramref name="attachment"/> from the <see cref="Storage.Message"/> object.
            /// </summary>
            /// <example>
            ///     message.DeleteAttachment(message.Attachments[0]);
            /// </example>
            /// <param name="attachment"></param>
            /// <exception cref="MRCannotRemoveAttachment">Raised when it is not possible to remove the <see cref="Storage.Attachment"/> or <see cref="Storage.Message"/> from
            /// the <see cref="Storage.Message"/></exception>
            public void DeleteAttachment(object attachment)
            {
                if (FileAccess == FileAccess.Read)
                    throw new MRCannotRemoveAttachment("Cannot remove attachments when the file is not opened in Write or ReadWrite mode");

                foreach (var attachmentObject in _attachments)
                {
                    if (attachmentObject.Equals(attachment))
                    {
                        string storageName;
                        var attach = attachmentObject as Attachment;
                        if (attach != null)
                        {
                            if (string.IsNullOrEmpty(attach.StorageName))
                                throw new MRCannotRemoveAttachment("The attachment '" + attach.FileName +
                                                                    "' can not be removed, the storage name is unknown");

                            storageName = attach.StorageName;
                            attach.Dispose();
                        }
                        else
                        {
                            var msg = attachmentObject as Message;
                            if (msg == null)
                                throw new MRCannotRemoveAttachment(
                                    "The attachment can not be removed, could not convert the attachment to an Attachment or Message object");

                            storageName = msg.StorageName;
                            msg.Dispose();
                        }

                        _attachments.Remove(attachment);
                        TopParent._rootStorage.Delete(storageName);
                        break;
                    }
                }
            }
            #endregion

            #region Copy
            /// <summary>
            /// Copies the given <paramref name="source"/> to the given <paramref name="destination"/>
            /// </summary>
            /// <param name="source"></param>
            /// <param name="destination"></param>
            private static void Copy(CFStorage source, CFStorage destination)
            {
                source.VisitEntries(action =>
                {
                    if (action.IsStorage)
                    {
                        var destinationStorage = destination.AddStorage(action.Name);
                        destinationStorage.CLSID = action.CLSID;
                        destinationStorage.CreationDate = action.CreationDate;
                        destinationStorage.ModifyDate = action.ModifyDate;
                        Copy(action as CFStorage, destinationStorage);
                    }
                    else
                    {
                        var sourceStream = action as CFStream;
                        var destinationStream = destination.AddStream(action.Name);
                        if (sourceStream != null) destinationStream.SetData(sourceStream.GetData());
                    }

                }, false);
            }
            #endregion

            #region Save
            /// <summary>
            /// Saves this <see cref="Storage.Message" /> to the specified <paramref name="fileName"/>
            /// </summary>
            /// <param name="fileName"> Name of the file. </param>
            public void Save(string fileName)
            {
                using (var saveFileStream = File.Open(fileName, FileMode.Create, FileAccess.ReadWrite))
                    Save(saveFileStream);
            }

            /// <summary>
            /// Saves this <see cref="Storage.Message"/> to the specified <paramref name="stream"/>
            /// </summary>
            /// <param name="stream"> The stream to save to. </param>
            public void Save(Stream stream)
            {
                if (IsTopParent)
                {
                    _compoundFile.Save(stream);
                }
                else
                {
                    var compoundFile = new CompoundFile();
                    var sourceNameIdStorage = TopParent._rootStorage.GetStorage(MapiTags.NameIdStorage);
                    var rootStorage = compoundFile.RootStorage;
                    var destinationNameIdStorage = rootStorage.AddStorage(MapiTags.NameIdStorage);

                    Copy(sourceNameIdStorage, destinationNameIdStorage);
                    Copy(_rootStorage, rootStorage);

                    var propertiesStream = rootStorage.GetStream(MapiTags.PropertiesStream);
                    var sourceData = propertiesStream.GetData();
                    var destinationData = new byte[sourceData.Length + 8];
                    Buffer.BlockCopy(sourceData, 0, destinationData, 0, 24);
                    Buffer.BlockCopy(sourceData, 24, destinationData, 32, sourceData.Length - 24);
                    propertiesStream.SetData(destinationData);

                    compoundFile.Save(stream);
                    compoundFile.Close();
                }
            }
            #endregion

            #region SetEmailSenderAndRepresentingSender
            /// <summary>
            /// Gets the <see cref="Sender"/> and <see cref="SenderRepresenting"/> from the <see cref="Storage.Message"/>
            /// object and sets the <see cref="Storage.Message.Sender"/> and <see cref="Storage.Message.SenderRepresenting"/>
            /// </summary>
            private void SetEmailSenderAndRepresentingSender()
            {
                var tempEmail = GetMapiPropertyString(MapiTags.PR_SENDER_EMAIL_ADDRESS);

                if (string.IsNullOrEmpty(tempEmail) || tempEmail.IndexOf('@') == -1)
                    tempEmail = GetMapiPropertyString(MapiTags.PR_SENDER_SMTP_ADDRESS);

                if (string.IsNullOrEmpty(tempEmail))
                    tempEmail = GetMapiPropertyString(MapiTags.InternetAccountName);

                if (string.IsNullOrEmpty(tempEmail))
                    tempEmail = GetMapiPropertyString(MapiTags.SenderSmtpAddressAlternate);

                MessageHeader headers = null;

                if (string.IsNullOrEmpty(tempEmail) || tempEmail.IndexOf("@", StringComparison.Ordinal) < 0)
                {
                    var senderAddressType = GetMapiPropertyString(MapiTags.PR_SENDER_ADDRTYPE);
                    if (senderAddressType != null && senderAddressType != "EX")
                    {
                        // Get address from email headers. The headers are not present when the addressType = "EX"
                        var header = GetStreamAsString(MapiTags.HeaderStreamName, Encoding.Unicode);
                        if (!string.IsNullOrEmpty(header))
                            headers = HeaderExtractor.GetHeaders(header);
                    }
                }

                // PR_PRIMARY_SEND_ACCT can contain the smtp address of an exchange account
                if (string.IsNullOrEmpty(tempEmail) || tempEmail.IndexOf("@", StringComparison.Ordinal) < 0)
                {
                    var testEmail = GetMapiPropertyString(MapiTags.PR_PRIMARY_SEND_ACCT);
                    if(!string.IsNullOrEmpty(testEmail) && testEmail.IndexOf("\u0001", StringComparison.Ordinal) > 0)
                    {
                        tempEmail = EmailAddress.GetValidEmailAddress(testEmail);
                    }
                }

                tempEmail = EmailAddress.RemoveSingleQuotes(tempEmail);
                var tempDisplayName = EmailAddress.RemoveSingleQuotes(GetMapiPropertyString(MapiTags.PR_SENDER_NAME));

                if (string.IsNullOrEmpty(tempEmail) && headers?.From != null)
                    tempEmail = EmailAddress.RemoveSingleQuotes(headers.From.Address);

                if (string.IsNullOrEmpty(tempDisplayName) && headers?.From != null)
                    tempDisplayName = headers.From.DisplayName;

                var email = tempEmail;
                var displayName = tempDisplayName;

                // Sometimes the E-mail address and displayname get swapped so check if they are valid
                if (!EmailAddress.IsEmailAddressValid(tempEmail) && EmailAddress.IsEmailAddressValid(tempDisplayName))
                {
                    // Swap then
                    email = tempDisplayName;
                    displayName = tempEmail;
                }
                else if (EmailAddress.IsEmailAddressValid(tempDisplayName))
                {
                    // If the displayname is an emailAddress then move it
                    email = tempDisplayName;
                    displayName = tempDisplayName;
                }

                if (string.Equals(tempEmail, tempDisplayName, StringComparison.InvariantCultureIgnoreCase))
                    displayName = string.Empty;

                // Set the representing sender if it is there
                Sender = new Sender(email, displayName);
                var representingAddressType = GetMapiPropertyString(MapiTags.PR_SENT_REPRESENTING_ADDRTYPE);
                tempEmail = GetMapiPropertyString(MapiTags.PR_SENT_REPRESENTING_EMAIL_ADDRESS);
                tempEmail = EmailAddress.RemoveSingleQuotes(tempEmail);
                tempDisplayName = EmailAddress.RemoveSingleQuotes(GetMapiPropertyString(MapiTags.PR_SENT_REPRESENTING_NAME));

                email = tempEmail;
                displayName = tempDisplayName;

                // Sometimes the E-mail address and displayname get swapped so check if they are valid
                if (!EmailAddress.IsEmailAddressValid(tempEmail) && EmailAddress.IsEmailAddressValid(tempDisplayName))
                {
                    // Swap then
                    email = tempDisplayName;
                    displayName = tempEmail;
                }
                else if (EmailAddress.IsEmailAddressValid(tempDisplayName))
                {
                    // If the displayname is an emailAddress then move it
                    email = tempDisplayName;
                    displayName = tempDisplayName;
                }

                if (string.Equals(tempEmail, tempDisplayName, StringComparison.InvariantCultureIgnoreCase))
                    displayName = string.Empty;

                // Set the representing sender
                if (!string.IsNullOrWhiteSpace(email))
                    SenderRepresenting = new SenderRepresenting(email, displayName, representingAddressType);
            }
            #endregion
            
            #region GetEmailSender
            /// <summary>
            /// Returns the E-mail sender address in RFC822 format, e.g. 
            /// "Pan, P (Peter)" &lt;Peter.Pan@neverland.com&gt;
            /// </summary>
            /// <returns></returns>
            public string GetEmailSenderRfc822Format()
            {
                var output = string.Empty;

                if (!string.IsNullOrEmpty(Sender.DisplayName))
                    output = "\"" + Sender.DisplayName + "\"";

                if (!string.IsNullOrEmpty(Sender.Email))
                {
                    if (!string.IsNullOrEmpty(output))
                        output += " ";

                    output += "<" + Sender.Email + ">";
                }

                return output;
            }

            /// <summary>
            /// Returns the E-mail sender address in a human readable format
            /// </summary>
            /// <param name="html">Set to true to return the E-mail address as an html string</param>
            /// <param name="convertToHref">Set to true to convert the E-mail addresses to a hyperlink. 
            /// Will be ignored when <paramref name="html"/> is set to false</param>
            /// <returns></returns>
            public string GetEmailSender(bool html, bool convertToHref)
            {
                var output = string.Empty;

                var emailAddress = Sender.Email;
                var representingEmailAddress = string.Empty;
                var displayName = Sender.DisplayName;
                var representingDisplayName = string.Empty;
                var representingAddressType = string.Empty;

                if (SenderRepresenting != null)
                {
                    representingEmailAddress = SenderRepresenting.Email;
                    representingDisplayName = SenderRepresenting.DisplayName;
                    representingAddressType = SenderRepresenting.AddressType;
                }

                if (html)
                {
                    emailAddress = WebUtility.HtmlEncode(emailAddress);
                    displayName = WebUtility.HtmlEncode(displayName);
                    representingEmailAddress = WebUtility.HtmlEncode(representingEmailAddress);
                    representingDisplayName = WebUtility.HtmlEncode(representingDisplayName);
                }

                // If we want hyperlinks and the outputformat is html and the email address is set
                if (convertToHref && html && 
                    !string.IsNullOrEmpty(emailAddress))
                {
                    output += "<a href=\"mailto:" + emailAddress + "\">" +
                                (!string.IsNullOrEmpty(displayName)
                                    ? displayName
                                    : emailAddress) + "</a>";

                    if (!string.IsNullOrEmpty(representingEmailAddress) && 
                        !string.IsNullOrEmpty(emailAddress) &&
                        !emailAddress.Equals(representingEmailAddress, StringComparison.InvariantCultureIgnoreCase))
                    {
                        output += " " + LanguageConsts.EmailOnBehalfOf + " <a href=\"mailto:" + representingEmailAddress +
                                    "\">" +
                                    (!string.IsNullOrEmpty(representingDisplayName)
                                        ? representingDisplayName
                                        : representingEmailAddress) + "</a> ";
                    }
                }
                else
                {
                    string beginTag;
                    string endTag;
                    if (html)
                    {
                        beginTag = "&nbsp;&lt;";
                        endTag = "&gt;";
                    }
                    else
                    {
                        beginTag = " <";
                        endTag = ">";
                    }

                    if (!string.IsNullOrEmpty(displayName))
                        output += displayName;

                    if (!string.IsNullOrEmpty(emailAddress))
                        output += beginTag + emailAddress + endTag;

                    if (!string.IsNullOrEmpty(representingEmailAddress) &&
                        !string.IsNullOrEmpty(emailAddress) &&
                        !emailAddress.Equals(representingEmailAddress, StringComparison.InvariantCultureIgnoreCase))
                    {
                        output += " " + LanguageConsts.EmailOnBehalfOf + " ";

                        if (!string.IsNullOrEmpty(representingDisplayName))
                            output += representingDisplayName;

                        if (!string.IsNullOrEmpty(representingEmailAddress) && representingAddressType != "EX")
                        {
                            if (!string.IsNullOrWhiteSpace(representingDisplayName)) output += beginTag;
                            output += representingEmailAddress;
                            if (!string.IsNullOrWhiteSpace(representingDisplayName)) output += endTag;
                        }
                    }
                }
                
                return output;
            }
            #endregion

            #region GetEmailRecipients
            /// <summary>
            /// Returns all the recipient for the given <paramref name="type"/>
            /// </summary>
            /// <param name="type">The <see cref="RecipientType"/> to return</param>
            /// <returns></returns>
            private List<RecipientPlaceHolder> GetEmailRecipients(RecipientType type)
            {
                var recipients = new List<RecipientPlaceHolder>();

                // ReSharper disable once LoopCanBeConvertedToQuery
                foreach (var recipient in Recipients)
                {
                    // First we filter for the correct recipient type
                    if (recipient.Type == type)
                        recipients.Add(new RecipientPlaceHolder(recipient.Email, recipient.DisplayName, recipient.AddressType));
                }

                if (recipients.Count == 0 && Headers != null)
                {
                    switch (type)
                    {
                        case RecipientType.To:
                            if (Headers.To != null)
                                recipients.AddRange(
                                    Headers.To.Select(
                                        to => new RecipientPlaceHolder(to.Address, to.DisplayName, string.Empty)));
                            break;

                        case RecipientType.Cc:
                            if (Headers.Cc != null)
                                recipients.AddRange(
                                    Headers.Cc.Select(
                                        cc => new RecipientPlaceHolder(cc.Address, cc.DisplayName, string.Empty)));
                            break;

                        case RecipientType.Bcc:
                            if (Headers.Bcc != null)
                                recipients.AddRange(
                                    Headers.Bcc.Select(
                                        bcc => new RecipientPlaceHolder(bcc.Address, bcc.DisplayName, string.Empty)));
                            break;
                    }
                }

                return recipients;
            }

            /// <summary>
            /// Returns the E-mail recipients in RFC822 format, e.g. 
            /// "Pan, P (Peter)" &lt;Peter.Pan@neverland.com&gt;
            /// </summary>
            /// <param name="type">Selects the Recipient type to retrieve</param>
            /// <returns></returns>
            public string GetEmailRecipientsRfc822Format(RecipientType type)
            {
                var output = string.Empty;

                var recipients = GetEmailRecipients(type);
                if (Appointment?.UnsendableRecipients != null)
                    recipients.AddRange(Appointment.UnsendableRecipients.GetEmailRecipients(type));

                foreach (var recipient in recipients)
                {
                    if (output != string.Empty)
                        output += ", ";

                    var tempOutput = string.Empty;

                    if (!string.IsNullOrEmpty(recipient.DisplayName))
                        tempOutput += "\"" + recipient.DisplayName + "\"";

                    if (!string.IsNullOrEmpty(recipient.Email))
                    {
                        if (!string.IsNullOrEmpty(tempOutput))
                            tempOutput += " ";

                        tempOutput += "<" + recipient.Email + ">";
                    }

                    output += tempOutput;
                }

                return output;
            }

            /// <summary>
            /// Returns the E-mail recipients in a human readable format
            /// </summary>
            /// <param name="type">Selects the Recipient type to retrieve</param>
            /// <param name="html">Set to true to return the E-mail address as an html string</param>
            /// <param name="convertToHref">Set to true to convert the E-mail addresses to hyperlinks. 
            /// Will be ignored when <param ref="html"/> is set to false</param>
            /// <returns></returns>
            public string GetEmailRecipients(RecipientType type,
                bool html,
                bool convertToHref)
            {
                var output = string.Empty;

                var recipients = GetEmailRecipients(type);
                if (Appointment?.UnsendableRecipients != null)
                    recipients.AddRange(Appointment.UnsendableRecipients.GetEmailRecipients(type));

                foreach (var recipient in recipients)
                {
                    if (output != string.Empty)
                        output += "; ";

                    var emailAddress = recipient.Email;
                    var displayName = recipient.DisplayName;

                    if (convertToHref && html && !string.IsNullOrEmpty(emailAddress))
                        output += "<a href=\"mailto:" + emailAddress + "\">" +
                                    (!string.IsNullOrEmpty(displayName)
                                        ? displayName
                                        : emailAddress) + "</a>";

                    else
                    {
                        if (!string.IsNullOrEmpty(displayName))
                            output += displayName;

                        var beginTag = string.Empty;
                        var endTag = string.Empty;
                        if (!string.IsNullOrEmpty(displayName))
                        {
                            if (html)
                            {
                                beginTag = "&nbsp;&lt;";
                                endTag = "&gt;";
                            }
                            else
                            {
                                beginTag = " <";
                                endTag = ">";
                            }
                        }

                        if (!string.IsNullOrEmpty(emailAddress))
                            output += beginTag + emailAddress + endTag;
                    }
                }

                return output;
            }
            #endregion

            #region GetAttachmentNames
            /// <summary>
            /// Returns the attachments names as a comma seperated string
            /// </summary>
            /// <returns></returns>
            public string GetAttachmentNames()
            {
                var result = new List<string>();

                foreach (var attachment in Attachments)
                {
                    // ReSharper disable once CanBeReplacedWithTryCastAndCheckForNull
                    if (attachment is Attachment)
                    {
                        var attach = (Attachment)attachment;
                        result.Add(attach.FileName);
                    }
                    // ReSharper disable once CanBeReplacedWithTryCastAndCheckForNull
                    else if (attachment is Message)
                    {
                        var msg = (Message)attachment;
                        result.Add(msg.FileName);
                    }
                }

                return string.Join(", ", result);
            }
            #endregion
        }
    }
}